// 17.5 IO库再探
/**
 * 在第8章中我们介绍了IO库的基本结构及其最常用的部分。在本节中，我们将介绍三个更特殊的IO库特性：格式控制、未格式化IO和随机访问。
 */

#include <iterator>
#include <vector>
#include <list>
#include <deque>
#include <forward_list>
#include <string>
#include <array>
#include <stack>
#include <queue>
#include <algorithm>
#include <numeric>
using std::swap;
using std::vector, std::list, std::deque, std::forward_list, std::string, std::array, std::stack, std::queue;
#include "../Chapter07/Sales_data.h"
#include <iostream>
using std::begin, std::cbegin, std::end, std::cend, std::find, std::accumulate, std::equal, std::fill, std::fill_n, std::back_inserter;
using std::cin, std::cout, std::endl;
using std::copy, std::replace, std::replace_copy;
#include <map>
#include <set>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <memory>
#include <new>
using namespace std;
#include <functional>
#include "../VisualStudio2012/15/Quote.h"
#include "../VisualStudio2012/12/TextQuery.h"
#include <tuple>
#include <bitset>
#include <regex>
#include <random>
#include <cmath>
#include <iomanip>
#include <cstdio>

int main()
{
    // 17.5.1 格式化输入与输出
    /*除了条件状态外（参见8.1.2节，第279页），每个iostream对象还维护一个格式状态来控制IO如何格式化的细节。格式状态控制格式化的某些方面，如整型值是几进制、浮点值的精度、一个输出元素的宽度等。
      标准库定义了一组操纵符（manipulator）（参见1.2节，第6页）来修改流的格式状态，如表17.7和表17.8所示。一个操纵符是一个函数或是一个对象，会影响流的状态，并能用作输入或输出运算符的运算对象。
        类似输入和输出运算符，操纵符也返回它所处理的流对象，因此我们可以在一条语句中组合操纵符和数据。
      我们已经在程序中使用过一个操纵符——endl，我们将它“写”到输出流，就像它是一个值一样。但endl不是一个普通值，而是一个操作：它输出一个换行符并刷新缓冲区。
    */

    // 很多操纵符改变格式状态
    /*操纵符用于两大类输出控制：控制数值的输出形式以及控制补白的数量和位置。大多数改变格式状态的操纵符都是设置/复原成对的；一个操纵符用来将格式状态设置为一个新值，
        而另一个用来将其复原，恢复为正常的默认格式。
      当操纵符改变流的格式状态时，通常改变后的状态对所有后续IO都生效。
      但是，很多程序（而且更重要的是，很多程序员）期望流的状态符合标准库正常的默认设置。在这些情况下，将流的状态置于一个非标准状态可能会导致错误。
        因此，通常最好在不再需要特殊格式时尽快将流恢复到默认状态。
    */

    // 控制布尔值的格式
    /*操纵符改变对象的格式状态的一个例子是boolalpha操纵符。默认情况下，bool值打印为1或0。一个true值输出为整数1，而false输出为0。我们可以通过对流使用boolalpha操纵符来覆盖这种格式：[插图]
    */
    cout << "default bool values: " << true << " " << false // default bool values: 1 0
         << "\nalpha bool values: " << boolalpha << true << " " << false << endl; // alpha bool values: true false
    /*一旦向cout“写入”了boolalpha，我们就改变了cout打印bool值的方式。后续打印bool值的操作都会打印true或false而非1或0。
      为了取消cout格式状态的改变，我们使用noboolalpha：[插图]
    */
    bool bool_val = true;
    cout << boolalpha            // 设置cout的内部状态
         << bool_val 
         << noboolalpha << endl; // 将内部状态恢复为默认格式
    
    // 指定整型值的进制
    /*默认情况下，整型值的输入输出使用十进制。我们可以使用操纵符hex、oct和dec将其改为十六进制、八进制或是改回十进制：[插图]
    */
    cout << "default: " << 20 << " " << 1024 << endl;        // default: 20 1024
    cout << "octal: " << oct << 20 << " " << 1024 << endl;   // octal: 24 2000
    cout << "hex: " << hex << 20 << " " << 1024 << endl;     // hex: 14 400
    cout << "decimal: " << dec << 20 << " " << 1024 << endl; // decimal: 20 1024
    // 操纵符hex、oct和dec只影响整型运算对象，浮点值的表示形式不受影响。

    // 在输出中指出进制
    /*默认情况下，当我们打印出数值时，没有可见的线索指出使用的是几进制。例如，20是十进制的20还是16的八进制表示？当我们按十进制打印数值时，打印结果会符合我们的期望。
        如果需要打印八进制值或十六进制值，应该使用showbase操纵符。
      当对流应用showbase操纵符时，会在输出结果中显示进制，它遵循与整型常量中指定进制相同的规范：
        · 前导0x表示十六进制。
        · 前导0表示八进制。
        · 无前导字符串表示十进制。
    */
    cout << showbase;   // 当打印整型值时显示进制
    cout << "default: " << 20 << " " << 1024 << endl;           // default: 20 1024
    cout << "in octal: " << oct << 20 << " " << 1024 << endl;   // in octal: 024 02000
    cout << "in hex: " << hex << 20 << " " << 1024 << endl;     // in hex: 0x14 0x400
    cout << "in decimal: " << dec << 20 << " " << 1024 << endl; // in decimal: 20 1024
    cout << noshowbase; // 恢复流状态
    /*默认情况下，十六进制值会以小写打印，前导字符也是小写的x。我们可以通过使用uppercase操纵符来输出大写的X并将十六进制数字a-f以大写输出：[插图]
    */
    cout << uppercase << showbase << hex << "printed in hexdecimal: " << 20 << " " << 1024
         << nouppercase << noshowbase << dec << endl;
         
    // 控制浮点数格式
    /*我们可以控制浮点数输出三个种格式：
        · 以多高精度（多少个数字）打印浮点值
        · 数值是打印为十六进制、定点十进制还是科学记数法形式
        · 对于没有小数部分的浮点值是否打印小数点
      默认情况下，浮点值按六位数字精度打印；如果浮点值没有小数部分，则不打印小数点；根据浮点数的值选择打印成定点十进制或科学记数法形式。标准库会选择一种可读性更好的格式：
        非常大和非常小的值打印为科学记数法形式，其他值打印为定点十进制形式。
    */

    // 指定打印精度
    /* 默认情况下，精度会控制打印的数字的总数。当打印时，浮点值按当前精度舍入而非截断。因此，如果当前精度为四位数字，则3.14159将打印为3.142；如果精度为三位数字，则打印为3.14。
       我们可以通过调用IO对象的precision成员或使用setprecision操纵符来改变精度。
       precision成员是重载的（参见6.4节，第206页）。一个版本接受一个int值，将精度设置为此值，并返回旧精度值。另一个版本不接受参数，返回当前精度值。setprecision操纵符接受一个参数，用来设置精度。
       操纵符setprecision和其他接受参数的操纵符都定义在头文件iomanip中。
    */
    // cout.precision返回当前精度值
    cout << "Precision: " << cout.precision() << ", Value: " << sqrt(2.0) << endl;
    // cout.percision(12)将打印精度设置为12位数字
    cout.precision(12);
    cout << "Precision: " << cout.precision() << ", Value: " << sqrt(2.0) << endl;
    // 另一种设置精度的方法是使用setprecision操纵符
    cout << setprecision(3); // iomanip头文件定义了setprecision操纵符
    cout << "Precision: " << cout.precision() << ", Value: " << sqrt(2.0) << endl;
    /*标准库sqrt函数，它定义在头文件cmath中。sqrt函数是重载的，不同版本分别接受一个float、double或long double参数，返回实参的平方根。
表17.17 定义在iostream中的操纵符
boolalpha        将true和false输出为字符串
noboolalpha      将true和false输出为1和0
showbase         对整型值输出表示进制的前缀
noshowbase       不生成表示进制的前缀
showpoint        对浮点值总是显示小数点
noshowpoint      只有当浮点值包含小数部分时才显示小数点
showpos          对非负数总是显示+
noshowpos        对非负数不显示+
uppercase        在十六进制中打印0X，在科学计数法中打印E
nouppercase      在十六进制中打印0x，在科学计数法中打印e
dec              整型值显示为十进制
hex              整型值显示为十六进制
oct              整型值显示为八进制
left             在值的右侧添加填充字符
right            在值的左侧添加填充字符
internal         在符号和值之间添加填充字符
fixed            浮点值显示为定点十进制
scientific       浮点值显示为科学记数法
hexfloat         浮点值显示为十六进制（C++11新特性）
defaultfloat     重置浮点值格式为十进制（C++11新特性）
unitbuf          每次输出操作后都刷新缓冲区
nounitbuf        恢复正常的缓冲区刷新机制
skipws           输入运算符跳过空白符
noskipws         输入运算符不跳过空白符
flush            刷新ostream缓冲区
ends             插入空字符，然后刷新osteram缓冲区
endl             插入换行，然后刷新osteram缓冲区
    */

    // 指定浮点数记数法
    /*除非你需要控制浮点数的表示形式（如，按列打印数据或打印表示金额或百分比的数据），否则由标准库选择记数法是最好的方式。
      通过使用恰当的操纵符，我们可以强制一个流使用科学记数法、定点十进制或是十六进制记数法。操纵符scientific改变流的状态来使用科学记数法。操纵符fixed改变流的状态来使用定点十进制。
      在新标准库中，通过使用hexfloat也可以强制浮点数使用十六进制格式。新标准库还提供另一个名为defaultfloat的操纵符，它将流恢复到默认状态——根据要打印的值选择记数法。
      这些操纵符也会改变流的精度的默认含义。在执行scientific、fixed或hexfloat后，精度值控制的是小数点后面的数字位数，
        而默认情况下精度值指定的是数字的总位数——既包括小数点之后的数字也包括小数点之前的数字。
    */
   cout << "default format: " << 100 * sqrt(2.0) << '\n'              // default format: 141.421
        << "scientific: " << scientific << 100 *sqrt(2.0) << '\n'     // scientific: 1.414214e+002
        << "fixed decimal: " << fixed << 100 * sqrt(2.0) << '\n'      // fixed decimal: 141.421356
        << "hexadecimal: " << hexfloat << 100 * sqrt(2.0) << '\n'     // hexadecimal: 0x1.1ad7bcp+7
        << "use defaults: "<< defaultfloat << 100 * sqrt(2.0)         // use defaults: 141.421
        << "\n\n";

    // 打印小数点
    // 默认情况下，当一个浮点值的小数部分为0时，不显示小数点。showpoint操纵符强制打印小数点：[插图]
    cout << 10.0 << endl;        // 打印10
    cout << showpoint << 10.0    // 打印10.0000
         << noshowpoint << endl; // 恢复小数点的默认格式

    // 输出补白
    /*当按列打印数据时，我们常常需要非常精细地控制数据格式。标准库提供了一些操纵符帮助我们完成所需的控制：
        · setw指定下一个数字或字符串值的最小空间。
        · left表示左对齐输出。
        · right表示右对齐输出，右对齐是默认格式。
        · internal控制负数的符号的位置，它左对齐符号，右对齐值，用空格填满所有中间空间。
        · setfill允许指定一个字符代替默认的空格来补白输出。
      setw类似endl，不改变输出流的内部状态。它只决定下一个输出的大小。
    */
    int i = -16;
    double d = 3.14159;
    // 补白第一列，试用输出中最小12个位置
    cout << "i: " << setw(12) << i << "next col" << '\n'
         << "d: " << setw(12) << d << "next col" << '\n';
    // 补白第一列，左对齐所有列
    cout << left 
         << "i: " << setw(12) << i << "next col" << '\n'
         << "d: " << setw(12) << d << "next col" << '\n'
         << right; // 恢复正常对齐
    // 补白第一列，右对齐所有列
    cout << right 
         << "i: " << setw(12) << i << "next col" << '\n'
         << "d: " << setw(12) << d << "next col" << '\n';
    // 补白第一列，但补在域的内部
    cout << internal 
         << "i: " << setw(12) << i << "next col" << '\n'
         << "d: " << setw(12) << d << "next col" << '\n';
    // 补白第一列，用#作为补白字符
    cout << setfill('#') 
         << "i: " << setw(12) << i << "next col" << '\n'
         << "d: " << setw(12) << d << "next col" << '\n'
         << setfill(' '); // 恢复正常的补白字符
    /*
表17.18 定义在iomanip中的操纵符
setfill(ch)     用ch填充空白
setprecision(n) 将浮点精度设置为n
setw(w)         读写值的宽度为w个字符
setbase(b)      将整数输出为b进制
    */

    // 控制输入格式
    /*默认情况下，输入运算符会忽略空白符（空格符、制表符、换行符、换纸符和回车符）。
      操纵符noskipws会令输入运算符读取空白符，而不是跳过它们。为了恢复默认行为，我们可以使用skipws操纵符：
    */
    cin >> noskipws; // 设置cin读取空白符
    char ch;
    while(cin >> ch)
      cout << ch;
    cin >> skipws;   // 将cin恢复到默认状态，从而丢弃空白符

    // 17.5.2 未格式化的输入/输出操作
    // 标准库还提供了一组低层操作，支持未格式化IO（unformatted IO）。这些操作允许我们将一个流当作一个无解释的字节序列来处理。
    
    // 单字节操作
    /*有几个未格式化操作每次一个字节地处理流。这些操作列在表17.19中，它们会读取而不是忽略空白符。例如，我们可以使用未格式化IO操作get和put来读取和写入一个字符：[插图]
表17.19 单字节低层IO操作
is.get(ch)      从istream is读取下一个字节存入字符ch中。返回is
os.put(ch)      将字符ch输出到ostream os。返回os
is.get()        将is的下一个字节作为int返回
is.putback(ch)  将字符ch放回is。返回is
is.unget()      将is向后移动一个字节。返回is
is.peek()       将下一个字节作为int返回，但不从流中删除它
    */
    char ch;
    while(cin.get(ch))
      cout.put(ch);
    // 此程序保留输入中的空白符，其输出与输入完全相同。它的执行过程与前一个使用noskipws的程序完全相同。

    // 将字符放回输入流
    /*有时我们需要读取一个字符才能知道还未准备好处理它。在这种情况下，我们希望将字符放回流中。标准库提供了三种方法退回字符，它们有着细微的差别：
        · peek返回输入流中下一个字符的副本，但不会将它从流中删除，peek返回的值仍然留在流中。
        · unget使得输入流向后移动，从而最后读取的值又回到流中。即使我们不知道最后从流中读取什么值，仍然可以调用unget。
        · putback是更特殊版本的unget：它退回从流中读取的最后一个值，但它接受一个参数，此参数必须与最后读取的值相同。
    */

    // 从输入操作返回的int值
    /*函数peek和无参的get版本都以int类型从输入流返回一个字符。这有些令人吃惊，可能这些函数返回一个char看起来会更自然。这些函数返回一个int的原因是：可以返回文件尾标记。
        我们使用char范围中的每个值来表示一个真实字符，因此，取值范围中没有额外的值可以用来表示文件尾。
      头文件cstdio定义了一个名为EOF的const，我们可以用它来检测从get返回的值是否是文件尾，而不必记忆表示文件尾的实际数值。对我们来说重要的是，用一个int来保存从这些函数返回的值：[插图]
    */
    int ch; // 使用一个int，而不是一个char来保存get()的返回值
    while((ch == cin.get()) != EOF)
      cout.put(ch);
    
    // 多字节操作
    /*一些未格式化IO操作一次处理大块数据。如果速度是要考虑的重点问题的话，这些操作是很重要的，但类似其他低层操作，这些操作也容易出错。特别是，
        这些操作要求我们自己分配并管理用来保存和提取数据的字符数组（参见12.2节，第423页）。表17.20列出了多字节操作。
表17.20 多字节低层IO操作
is.get(sink, size, delim)
    从is中读取最多size个字节，并保存在字符数组中，字符数组的起始地址由sink给出。读取过程直至遇到字符delim或读取了size个字节或遇到文件尾时停止。
    如果遇到了delim，则将其留在输入流中，不读取出来存入sink
is.getline(sink, size, delim)
    与接受三个参数的get版本类似，但会读取并丢弃delim
is.read(sink, size)
    读取最多size个字节，存入字符数组sink中。返回is
is.gcount()
    返回上一个未格式化读取操作从is读取的字节数
os.write(source, size)
    将字符数组source中的size个字节写入os。返回os
is.ignore(size, delim)
    读取并忽略最多size个字符，包括delim。与其他未格式化函数不同，ignore有默认参数：size的默认值为1，delim的默认值为文件尾
    */

    // 确定读取了多少个字符
    /*某些操作从输入读取未知个数的字节。我们可以调用gcount来确定最后一个未格式化输入操作读取了多少个字符。应该在任何后续未格式化输入操作之前调用gcount。
        特别是，将字符退回流的单字符操作也属于未格式化输入操作。如果在调用gcount之前调用了peek、unget或putback，则gcount的返回值为0。
    */

    // 小心：低层函数容易出错
    /*一般情况下，我们主张使用标准库提供的高层抽象。返回int的IO操作很好地解释了原因。一个常见的编程错误是将get或peek的返回值赋予一个char而不是一个int。这样做是错误的，但编译器却不能发现这个错误。
        最终会发生什么依赖于程序运行于哪台机器以及输入数据是什么。例如，在一台char被实现为unsigned char的机器上，下面的循环永远不会停止：[插图]
    */
    char ch; // 此处用char就是引入灾难！
    // 从cin.get()返回的值被转换为char，然后与一个int比较
    while((ch = cin.get()) != EOF)
      cout.put(ch);
    // 问题出在当get返回EOF时，此值会被转换为一个unsigned char。转换得到的值与EOF的int值不再相等，因此循环永远也不会停止。这种错误很可能在调试时发现。
    /*在ANSI C标准中，char类型的符号性（signed或unsigned）是实现定义的，这意味着不同的编译器和平台可以自由选择将char默认实现为signed char或unsigned char。
        然而，在某些机器和编译器上，char默认被实现为signed char。这通常是历史遗留习惯或者与特定架构的处理器设计有关。例如，在一些基于Intel x86架构的系统以及某些Unix/Linux系统上，
        char默认为signed char。要确保字符类型的行为，最好在编程时明确使用signed char或unsigned char，而不是依赖于默认的实现定义行为。这样可以提高代码的可移植性和清晰性。
    */

    // 17.5.3 流随机访问
    /*各种流类型通常都支持对流中数据的随机访问。我们可以重定位流，使之跳过一些数据，首先读取最后一行，然后读取第一行，依此类推。
        标准库提供了一对函数，来定位（seek）到流中给定的位置，以及告诉（tell）我们当前位置。
      随机IO本质上是依赖于系统的。为了理解如何使用这些特性，你必须查询系统文档。
      虽然标准库为所有流类型都定义了seek和tell函数，但它们是否会做有意义的事情依赖于流绑定到哪个设备。在大多数系统中，绑定到cin、cout、cerr和clog的流不支持随机访问——
        毕竟，当我们向cout直接输出数据时，类似向回跳十个位置这种操作是没有意义的。
      由于istream和ostream类型通常不支持随机访问，所以本节剩余内容只适用于fstream和sstream类型。
    */

    // seek和tell函数
    /*为了支持随机访问，IO类型维护一个标记来确定下一个读写操作要在哪里进行。它们还提供了两个函数：一个函数通过将标记seek到一个给定位置来重定位它；另一个函数tell我们标记的当前位置。
        标准库实际上定义了两对seek和tell函数，如表17.21所示。一对用于输入流，另一对用于输出流。输入和输出版本的差别在于名字的后缀是g还是p。
        g版本表示我们正在“获得”（读取）数据，而p版本表示我们正在“放置”（写入）数据。
表17.21 seek和tell函数
tellg()            返回一个输入流中（tellg）或输出流中（tellp）标记的当前位置
tellp()
seekg(pos)         在一个输入流或输出流中将标记重定位到给定的绝对地址。pos通常是前一个tellg或tellp返回的值
seekp(pos)
seekp(off，from)   在一个输入流或输出流中将标记定位到from之前或之后off个字符，from可以是下列值之一
seekg(off，from)     - beg，偏移量相对于流开始位置
                       cur，偏移量相对于流当前位置
                       end，偏移量相对于流结尾位置
      从逻辑上讲，我们只能对istream和派生自istream的类型ifstream和istringstream（参见8.1节，第278页）使用g版本，同样只能对ostream和派生自ostream的类型ofstream和ostringstream使用p版本。
        一个iostream、fstream或stringstream既能读又能写关联的流，因此对这些类型的对象既能使用g版本又能使用p版本。
    */

    // 只有一个标记
    /*当我们处理一个只读或只写的流时，两种版本的区别甚至是不明显的。我们可以对这些流只使用g或只使用p版本。如果我们试图对一个ifstream流调用tellp，编译器会报告错误。
        类似的，编译器也不允许我们对一个ostringstream调用seekg。
      fstream和stringstream类型可以读写同一个流。在这些类型中，有单一的缓冲区用于保存读写的数据，同样，标记也只有一个，表示缓冲区中的当前位置。标准库将g和p版本的读写位置都映射到这个单一的标记。
      由于只有单一的标记，因此只要我们在读写操作间切换，就必须进行seek操作来重定位标记。
    */

    // 重定位标记
    /*seek函数有两个版本：一个移动到文件中的“绝对”地址；另一个移动到一个给定位置的指定偏移量：[插图]
// 将标记移动到一个固定位置
seekg(new_position); // 将读取标记移动到指定的pos_type类型的位置
seekp(new_position); // 将写入标记移动到指定的pos_type类型的位置
// 移动到给定起始点之前或之后指定的偏移位置
seekg(offset, from); // 将读取标记移动到距from偏移量为offset的位置，offset类型为off_type
seekp(offset, from); // 将写入标记移动到距from偏移量为offset的位置，offset类型为off_type
      参数new_position和offset的类型分别是pos_type和off_type，这两个类型都是机器相关的，它们定义在头文件istream和ostream中。pos_type表示一个文件位置，而off_type表示距当前位置的一个偏移量。
        一个off_type类型的值可以是正的也可以是负的，即，我们可以在文件中向前移动或向后移动。
    */

    // 访问标记
    // 函数tellg和tellp返回一个pos_type值，表示流的当前位置。tell函数通常用来记住一个位置，以便稍后再定位回来：[插图]
    ostringstream writeStr; // 输出stringstream
    ostringstream::pos_type mark = writeStr.tellp();
    // ...
    writeStr.seekp(mark);   // 回到刚才记住的位置

    // 读写同一个文件
    // 以读写方式打开文件，并定位到文件尾
    fstream inOut("copyOut", fstream::ate | fstream::in | fstream::out); // 文件模式参数参见8.2.2节
    if(!inOut) {
      cerr << "can't open file" << endl;
      return EXIT_FAILURE; // EXIT_FAILURE参见6.3.2节
    }
    // inOut以ate模式打开，因此一开始就定义到其文件尾
    auto end_mark = inOut.tellg(); // 记住原文件尾位置
    inOut.seekg(0, fstream::beg);  // 重定位到文件开始
    size_t cnt = 0;                // 字节数累加器
    string line;                   // 保存输入中的每行
    // 继续读取的条件：还未遇到错误且还在读取原数据
    while(inOut && inOut.tellg()!= end_mark 
                && getline(inOut, line)) { // 且还可获取一行输入
      cnt += line.size() + 1;              // +1表示换行符
      auto mark = inOut.tellg();           // 记住读取位置
      inOut.seekp(0, fstream::end);        // 将写标记移动到文件尾
      inOut << cnt;                        // 输出累计的长度
      // 如果不是最后一行，打印一个分隔符
      if(mark != end_mark) inOut << " ";
      inOut.seekg(mark);                   // 恢复读取位置
    }
    inOut.seekp(0, fstream::end);          // 定位到文件尾
    inOut << "\n";                         // 在文件尾输出一个换行符x


    return 0;
}